<template>
  <div style="overflow: hidden;">
    <div class="toolbar" v-if="mode !== 'display'">
      <div data-testid="add-node" :class="`toolbar-button ${editMode === 'addNode' ? 'active' : ''}`" @click="handleClickAddNode()">
        <el-icon type="text" class="el-icon-circle-plus-outline"></el-icon>
      </div>
      <el-divider direction="vertical"></el-divider>
      <div data-testid="add-edge" :class="`toolbar-button ${editMode === 'addEdge' ? 'active' : ''}`" @click="handleClickAddEdge()">
        <el-icon type="text" class="el-icon-right"></el-icon>
      </div>
      <el-divider direction="vertical"></el-divider>
      <el-button data-testid="delete" type="text" @click="removeSelected()" icon="el-icon-delete"></el-button>
      <el-divider direction="vertical"></el-divider>
      <el-button type="text" @click="normalize()" icon="el-icon-s-grid"></el-button>

    </div>
    <div :id="containerId" data-testid="task-map" style="height: 600px;"></div>

  </div>

</template>

<script>
import G6 from "@antv/g6";
export default {
  name: "TaskMap",
  props: {
    mode: {
      default(){
        return 'addEdge';
      }, // 有两种可选模式： display, edit
    },
    nodes: {
      default(){
        return []
      }
    },
    getEmptyNode: {
      default(){
        return () => {
          return {
            id: Math.floor(Math.random() * 10000000) * 1000000 + new Date().getTime(),
            dependencies: []
          }
        }
      }
    },
    tooltipConfig: {
      default: undefined
    },
    clickMenuConfig: {
      default: undefined
    },
    nodeStateStyles: {
      default: undefined
    },
    nodeStates: {
      default(){
        return [];
      }
    },
    getNodeLabel: {
      default(){
        return node => undefined;
      }
    }
  },
  model: {
    prop: 'nodes',
    event: 'change'
  },
  data() {
    return {
      containerId: `${Math.floor(Math.random() * 10000000) }${new Date().getTime()}`,
      graph: undefined,
      handleResize: () => {
        if (!this.graph || this.graph.get('destroyed')) return;
        const container = document.getElementById(this.containerId);

        if (!container || !container.offsetWidth || !container.offsetHeight) return;
        this.graph.changeSize(container.offsetWidth, container.offsetHeight - 30);
        if(this.mode === 'display'){
          setTimeout(() => {
            this.graph.fitView();
          }, 1)

        }
      },
      highlightPath: [],
      defaultNodeStyle: {
        fill: '#b8d8ff',
        stroke: '#007aff'
      },
      highlightNodeStyle: {
        fill: '#ffb1b1',
        stroke: '#ff0000'
      },
      editMode: 'default'
    }
  },
  mounted() {
    window.addEventListener('resize', this.handleResize);
    this.registerBehavior();
    this.setUpMap();
    if(this.mode === 'display'){
      this.normalize();
    }
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.handleResize);
    this.graph.destroy();
  },
  methods: {
    normalize(){
      this.graph.updateLayout({
        type: 'dagre',
        rankdir: 'LR',
        align: 'DL',
        nodesepFunc: () => 1,
        ranksepFunc: () => 1,
      });
      this.graph.destroyLayout();
      setTimeout(() => {
        if(this.graph && !this.graph.get('destroyed')){
          this.graph.fitView();
        }
      }, 1)
    },
    emitNodeChange(){
      this.$emit('change', this.nodes)
    },
    setUpMap(){
      const container = document.getElementById(this.containerId);
      const width = container.offsetWidth;
      const height = (container.offsetHeight || 500) - 30;
      const graph = new G6.Graph({
        container: this.containerId,
        width,
        height,
        plugins: this.graphPlugins,
        // The sets of behavior modes
        modes: {
          // Defualt mode
          default: ['drag-node', 'click-select', 'drag-canvas', 'zoom-canvas', 'click-select-edge', 'send-node-click-event'],
          // Adding node mode
          addNode: ['zoom-canvas', 'drag-canvas', 'drag-node','click-add-node', 'click-select', 'click-select-edge'],
          // Adding edge mode
          addEdge: ['click-add-edge', 'click-select-edge', 'click-select'],

          display: ['send-node-click-event']
        },
        // The node styles in different states
        nodeStateStyles: {
          // The node styles in selected state
          selected: {
            ...this.defaultNodeStyle,
            lineWidth: 2,
            shadowColor: '#9e9e9e',
            shadowBlur: 10
          },

          highlighted: this.highlightNodeStyle,
          ...this.nodeStateStyles
        },
        defaultNode: {
          style: this.defaultNodeStyle
        },
        defaultEdge: {
          size: 1,
          color: '#e2e2e2',
          style: {
            startArrow: {
              path: 'M 0,0 L 8,4 L 8,-4 Z',
              fill: '#e2e2e2',
            },
          },
        },
      });
      graph.data({
        nodes: this.graphNodes,
        edges: this.graphEdges
      });
      graph.setMode(this.graphMode);
      graph.render();


      this.graph = graph;
      this.applyState(this.nodeStates, true);
    },
    registerBehavior(){
      const that = this;
      G6.registerBehavior('send-node-click-event', {
        getEvents(){
          return {
            'node:click': 'onClick'
          }
        },

        onClick(ev){
           const targetNodeId = Number(ev.item.getModel().id.split('-')[1]);
           that.$emit('click-node', targetNodeId);
        }
      })

      G6.registerBehavior('click-add-node', {
        // Set the events and the corresponding responsing function for this behavior
        getEvents() {
          // The event is canvas:click, the responsing function is onClick
          return {
            'canvas:click': 'onClick',
          };
        },
        // Click event
        onClick(ev) {
          const self = this;
          const graph = self.graph;
          // Add a new node
          const newNode = that.getEmptyNode();
          that.nodes.push(newNode);
          graph.addItem('node', {
            x: ev.x,
            y: ev.y,
            id: `node-${newNode.id}`, // Generate the unique id
          }).setState('selected', true);
          that.emitNodeChange();
          that.$emit('add-node', newNode.id);
        },
      });

      G6.registerBehavior('click-select-edge', {
        // Set the events and the corresponding responsing function for this behavior
        getEvents() {
          // The event is canvas:click, the responsing function is onClick
          return {
            'canvas:click': 'onCanvasClick',
            'edge:click': 'onEdgeClick'
          };
        },
        // Click event
        onCanvasClick(ev) {
          const graph = this.graph;
          graph.findAllByState('edge', 'selected').forEach((edge) => {
            graph.setItemState(edge, 'selected', false);
          });
        },

        onEdgeClick(ev){
          const graph = this.graph;
          graph.setItemState(ev.item, 'selected', ev.item.getStates().indexOf('selected') === -1);
        }
      });

      G6.registerBehavior('click-add-edge', {
        // Set the events and the corresponding responsing function for this behavior
        getEvents() {
          return {
            'node:click': 'onClick', // The event is canvas:click, the responsing function is onClick
            mousemove: 'onMousemove', // The event is mousemove, the responsing function is onMousemove
            'edge:click': 'onEdgeClick', // The event is edge:click, the responsing function is onEdgeClick
          };
        },
        // The responsing function for node:click defined in getEvents
        onClick(ev) {
          const self = this;
          const node = ev.item;
          const graph = self.graph;
          // The position where the mouse clicks
          const point = { x: ev.x, y: ev.y };
          const model = node.getModel();
          if (self.addingEdge && self.edge) {
            const dependencyId = Number.parseInt(model.id.split('-')[1]);
            const beginId = Number.parseInt(self.edge.getModel().target.split('-')[1]);
            if(that.checkValidEdge(beginId, dependencyId)){
              graph.updateItem(self.edge, {
                source: model.id,
              });
              that.nodes.filter(node => {
                return node.id === beginId;
              })[0].dependencies.push(dependencyId);
              that.emitNodeChange();
              self.edge = null;
              self.addingEdge = false;
            }
          } else {

            // Add anew edge, the end node is the current node user clicks
            self.edge = graph.addItem('edge', {
              source: point,
              target: model.id,
            });
            self.addingEdge = true;
          }
        },
        // The responsing function for mousemove defined in getEvents
        onMousemove(ev) {
          const self = this;
          // The current position the mouse clicks
          const point = { x: ev.x, y: ev.y };
          if (self.addingEdge && self.edge) {
            // Update the end node to the current node the mouse clicks
            self.graph.updateItem(self.edge, {
              source: point,
            });
          }
        },
        // The responsing function for edge:click defined in getEvents
        onEdgeClick(ev) {
          const self = this;
          const currentEdge = ev.item;
          if (self.addingEdge && self.edge === currentEdge) {
            self.graph.removeItem(self.edge);
            self.edge = null;
            self.addingEdge = false;
          }
        },
      });
    },
    registerNode(){
      G6.registerNode('rect-xml', {
        jsx: (cfg) => `
    <group>
      <rect style={{
        width: 200,
        height: 75,
      }}>
        <rect style={{
          width: 150,
          height: 20,
          fill: ${cfg.color},
          radius: [6, 6, 0, 0],
          cursor: 'move',
          stroke: ${cfg.color}
        }} draggable="true">
          <text style={{
            marginTop: 2,
            marginLeft: 75,
            textAlign: 'center',
            fontWeight: 'bold',
            fill: '#fff' }}>{{label}}</text>
        </rect>
        <rect style={{
          width: 150,
          height: 55,
          stroke: ${cfg.color},
          fill: #ffffff,
          radius: [0, 0, 6, 6],
        }}>
          <text style={{ marginTop: 5, marginLeft: 3, fill: '#333', marginLeft: 4 }}>描述: {{description}}</text>
          <text style={{ marginTop: 10, marginLeft: 3, fill: '#333', marginLeft: 4 }}>创建者: {{meta.creatorName}}</text>
        </rect>
      </rect>
    </group>
  `,
        afterDraw: (cfg, group) => {
          console.log(group);
          const img = group.findAllByName('img');
          if (img[0]) {
            img[0].animate(
                (ratio) => {
                  return {
                    opacity: Math.abs(0.5 - ratio),
                  };
                },
                {
                  duration: 3000,
                  repeat: true,
                },
            );
          }
        },
      });
    },
    checkValidEdge(fromId, toId){
      const nodeSet = {};
      this.nodes.forEach(node => {
        nodeSet[node.id] = node;
      })
      let path = [toId];
      if(this.isReachAble(nodeSet, toId, fromId, path)){
        this.highlightPath = path.map(id => this.graph.findById(`node-${id}`));
        this.$message({
          type: 'error',
          showClose: true,
          duration: 10000,
          onClose: () => {
            this.highlightPath = []
          },
          message: '不能添加该依赖关系，因为它将导致循环依赖！'
        })

        return false;
      }
      return true;
    },
    isReachAble(nodeSet, currentNodeId, targetNodeId, path){
      if(currentNodeId === targetNodeId){
        return true;
      }else{
        const unvisitedNodes = nodeSet[currentNodeId].dependencies.filter(id => {
          return path.filter(previousId => {
            return previousId === id;
          });
        });
        for(let i = 0; i < unvisitedNodes.length; i++){
          const id = unvisitedNodes[i];
          path.push(id);
          if(this.isReachAble(nodeSet, id, targetNodeId, path)){
            return true;
          }
          path.pop();
        }
        return false;

      }
    },
    removeSelected(){
      const selectedNodes = this.graph.findAllByState('node', 'selected');
      const nodeIdToRemove = selectedNodes.map(node => Number(node.getModel().id.split('-')[1]));
      const selectedEdges = this.graph.findAllByState('edge', 'selected');
      const dependenciesToRemove = {}
      selectedEdges.forEach(edge => {
        const { source, target } = edge.getModel();
        const sourceId = Number(source.split('-')[1]);
        const targetId = Number(target.split('-')[1])
        if(dependenciesToRemove[targetId]){
          dependenciesToRemove[targetId].push(sourceId);
        }else{
          dependenciesToRemove[targetId] = [sourceId];
        }
      })
      const leftNodes = this.nodes.filter(node => nodeIdToRemove.indexOf(node.id) === -1);
      leftNodes.forEach(node => {
        node.dependencies = node.dependencies.filter(id => {
          return nodeIdToRemove.indexOf(id) === -1 && (dependenciesToRemove[node.id] === undefined || dependenciesToRemove[node.id].indexOf(id) === -1)
        });
      });
      console.log(leftNodes);
      this.$emit('change', leftNodes);
    },
    handleClickAddNode(){
      if(this.editMode === 'addNode'){
        this.editMode = 'default'
      }else{
        this.editMode = 'addNode';
      }
    },
    handleClickAddEdge(){
      if(this.editMode === 'addEdge'){
        this.editMode = 'default'
      }else{
        this.editMode = 'addEdge'
      }
    },
    applyState(states, flag){
      const nodes = this.graph.getNodes();
      nodes.forEach(node => {
        Object.keys(states).forEach(state => {
          if(states[state].has(node.getModel().meta.id)){
            this.graph.setItemState(node, state, flag);
          }
        });
      });
    }
  },
  watch: {
    graphMode(newVal){
      if(this.graph){
        this.graph.setMode(newVal);
        if(newVal === 'display'){
          this.normalize();
        }
      }
    },
    highlightPath(newVal, oldVal){
      oldVal.forEach(node => {
        this.graph.setItemState(node, 'highlighted', false)
      });
      newVal.forEach(node => {
        this.graph.setItemState(node, 'highlighted', true)
      })
    },
    nodes(newVal){
      this.graph.changeData({
        nodes: this.graphNodes,
        edges: this.graphEdges
      });
      this.applyState(this.nodeStates, true)
      if(this.mode === 'display'){
        this.normalize();
      }
    },
    nodeStates(newVal, oldVal){
      this.applyState(oldVal, false);
      this.applyState(newVal, true);
    }
  },
  computed: {
    graphNodes(){
      return this.nodes.map(node => {
        return {
          id: `node-${node.id}`,
          meta: node,
          label: this.getNodeLabel(node)
        }
      })
    },
    graphEdges(){
      const result = []
      this.nodes.forEach(node => {
        result.push(...node.dependencies.map(dependency => {
          return {
            source: `node-${dependency}`,
            target: `node-${node.id}`
          }
        }))
      });
      return result;
    },
    graphMode(){
      if(this.mode === 'display'){
        return 'display';
      }else{
        return this.editMode
      }
    },
    graphPlugins(){
      const plugins = [];
      const tooltipConfig = this.tooltipConfig;
      const clickMenuConfig = this.clickMenuConfig;
      if(clickMenuConfig){
        plugins.push(new G6.Menu({
          offsetX: 6,
          offsetY: 10,
          itemTypes: ['node'],
          trigger: 'click',
          getContent(e) {
            return clickMenuConfig.getContent(e.item.getModel().meta);
          },
          handleMenuClick(target, item) {
            return clickMenuConfig.handleMenuClick(target, item.getModel().meta);
          },
        }))
      }

      if(tooltipConfig){
        plugins.push(new G6.Tooltip({
          offsetX: 10,
          offsetY: 20,
          getContent(e) {
            return tooltipConfig.getContent(e.item.getModel().meta);
          },
          itemTypes: ['node']
        }));
      }

      return plugins
    }
  }
}
</script>

<style scoped>
.toolbar{
  display: flex;
  flex-direction: row;
  align-items: center;
}
.active{
  background-color: #eeeeee;
}
.toolbar-button{
  padding: 4px 2px;
  border-radius: 5px
}
</style>
